iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
生成式 AI

iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線系列 第 19

Day 19 - 第一次接觸:使用 async/await 呼叫你的第一個生成式 AI API

  • 分享至 

  • xImage
  •  

從今天開始,我們要賦予我們的 App思考的能力。我們要讓它不再只是一個工具,而是一個能與我們對話的夥伴。

  • 困惑點:我的 App 要怎麼跟「外面」的世界說話?API 聽起來是在「雲端」或「網路上」的一個東西。那我的 App 要怎麼跟它溝通?

  • AI 協作

    • 我的 Prompt:我之前只會寫在手機裡運行的程式。現在我需要讓 App 去跟網路上的 AI API 拿資料,我不知道第一步該怎麼做。請問在 Swift 中,要如何處理這種『需要去網路上拿東西回來』的任務?請簡單說明並不需要給程式碼

    AI 的回答:

    您好,很高興能為您解答關於 App 與網路 API 溝通的問題。您提到的「需要去網路上拿東西回來」的任務,在程式設計中稱為非同步(Asynchronous)操作。

    為什麼需要非同步?

    簡單來說,手機 App 的使用者介面(UI)是在一個稱為主執行緒(Main Thread)的單一執行緒上運作的。如果我們直接在這個主執行緒上執行網路請求,程式會因為等待資料回傳而卡住,導致 UI 凍結,使用者會看到 App 完全沒有反應,這會帶來非常糟糕的使用者體驗。

    為了避免這種情況,我們需要將耗時的任務(例如網路請求)放到背景執行緒(Background Thread)去執行,讓主執行緒可以繼續處理 UI 更新和使用者互動。當背景任務完成後,再通知主執行緒去處理回傳的資料。

    Swift 的非同步處理方式

    在 Swift 中,處理這種非同步任務最現代且推薦的方式就是 async/await

    1. async 關鍵字:您可以在函式(Function)或方法(Method)前面加上 async,這表示這個函式是一個非同步函式,它在執行過程中可能會被「暫停」(suspend),等待某些耗時的操作完成,例如網路請求。
    2. await 關鍵字:當您在呼叫一個 async 函式時,您需要使用 await 關鍵字。await 會暫停當前程式碼的執行,直到它所呼叫的 async 函式完成並回傳結果。這就是 async/await 語法的核心概念:它讓非同步程式碼看起來像是循序執行(Synchronous),大大提升了可讀性。

    舉例來說,當您發起一個網路請求時,await 會暫停執行,讓您的程式可以去處理其他任務,當資料回傳後,程式會從 await 的地方繼續執行,去處理回來的資料。

    這種機制讓您不必手動處理複雜的回呼函式(Completion Handlers)或巢狀結構,使程式碼更簡潔、易於維護,也降低了出現錯誤的機率。

    網路請求的流程概觀

    要讓 App 去跟網路上的 AI API 拿資料,大致的步驟會是:

    1. 建立 URLRequest:設定好 API 的網址(URL)、請求方法(GET/POST 等),以及需要的請求頭(Headers)和請求主體(Body)。
    2. 執行網路請求:使用像是 URLSession 這類的 API,發起您的 async 網路請求。
    3. 使用 await 等待結果:在您呼叫網路請求函式時,使用 await 等待資料回傳。
    4. 處理回傳資料:當資料回傳後,您會收到一個 Data 物件,通常是 JSON 格式。您需要將這個 Data 解析(Decode)成 App 中可用的資料模型(Data Model)。這個過程也通常需要使用 async/await

    如果您有進一步的疑問,或是想深入了解如何將 async/await 應用在具體程式碼中,我會樂意為您提供更詳細的說明。

    • 根據 AI 的建議,我把詳細的實作步驟整理如下:

      1. 建立通用的 NetworkManager:打造一個可重複使用的網路請求底層,負責發送所有請求。
      2. 建立專門的 GeminiAPIService:建立一個專門與 Gemini API 溝通的服務,負責處理所有與 Gemini 相關的邏輯。
      3. 在 UI 中呼叫:最後,在我們的 ViewController 中,用最簡潔的方式呼叫服務,並將結果顯示在畫面上。

步驟一:建立一個通用的 NetworkManager

專業的 App 不會把 URLSession 的程式碼散落在各處。我們會建立一個通用的網路管理員,並搭配一個自訂的錯誤型別 APPError,讓錯誤處理更清晰。

檔案:APPError.swift (新增)

import Foundation

enum APPError: Error {
    case invalidURL
    case serverError
    case decodingError
    // ... 可根據需要新增更多錯誤類型
}

檔案:NetworkManager.swift (新增)

import Foundation

class NetworkManager {
    static let shared = NetworkManager()
    private init() {}

    func request<T: Decodable>(endpoint: String, method: String = "GET", headers: [String: String]? = nil, body: Data? = nil) async throws -> T {
        guard let url = URL(string: endpoint) else {
            throw APPError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.allHTTPHeaderFields = headers
        request.httpBody = body

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            throw APPError.serverError
        }
        
        do {
            let decodedData = try JSONDecoder().decode(T.self, from: data)
            return decodedData
        } catch {
            throw APPError.decodingError
        }
    }
}

步驟二:建立專門的 GeminiAPIService

有了通用的 NetworkManager,我們接著建立一個專門負責與 Gemini API 溝通的 Service

檔案:GeminiDataModels.swift (新增)

import Foundation

// --- 用於建立請求 (Request) 的資料結構 ---
struct GeminiRequest: Codable {
    let contents: [Content]
}

struct Content: Codable {
    let parts: [Part]
}

struct Part: Codable {
    var text: String? = nil
    var inlineData: InlineData? = nil
}

struct InlineData: Codable {
    let mimeType: String
    let data: String
}

// --- 用於解析回應 (Response) 的資料結構 ---
struct GeminiResponse: Codable {
    let candidates: [Candidate]
}

struct Candidate: Codable {
    let content: Content
}

檔案:GeminiAPIService.swift (新增)

import Foundation

class GeminiAPIService {
    private let networkManager = NetworkManager.shared
    private let apiKey: String

    init() {
        // 從 API_KEY.plist 安全地讀取金鑰
        guard let path = Bundle.main.path(forResource: "API_KEY", ofType: "plist"),
              let dict = NSDictionary(contentsOfFile: path),
              let key = dict["GeminiAPIKey"] as? String else {
            fatalError("無法在 API_KEY.plist 中找到 GeminiAPIKey")
        }
        self.apiKey = key
    }

    func generateContent(prompt: String) async throws -> GeminiResponse {
        let endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent"
        let headers = ["Content-Type": "application/json", "x-goog-api-key": apiKey]
        
        let requestBody = GeminiRequest(contents: [Content(parts: [Part(text: prompt)])])
        let bodyData = try JSONEncoder().encode(requestBody)

        let response: GeminiResponse = try await networkManager.request(
            endpoint: endpoint,
            method: "POST",
            headers: headers,
            body: bodyData
        )
        return response
    }
}

步驟三:在 ViewController 中呼叫

檔案:AITestViewController.swift (新增)

import UIKit

class AITestViewController: UIViewController {
    
    // MARK: - IBOutlet
    
    @IBOutlet weak var btnSent: UIButton!
    @IBOutlet weak var txfResult: UITextView!
    // MARK: - Property
    private let geminiService = GeminiAPIService()

    // MARK: - LifeCycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    // MARK: - UI Setting
    
    // MARK: - IBAcion
    @IBAction func sendTapped(_ sender: Any) {
        txfResult.text = "AI 正在思考中..."
        
        Task {
            do {
                let response = try await geminiService.generateContent(prompt: "用繁體中文講一句有關於Swift的冷知識")
                if let aiText = response.candidates.first?.content.parts.first?.text {
                    await MainActor.run {
                        txfResult.text = aiText
                    }
                }
            } catch {
                await MainActor.run {
                    txfResult.text = "發生錯誤: \(error.localizedDescription)"
                }
            }
        }
    }
    // MARK: - Function
    
}
// MARK: - Extensions

!!務必燒到實體機使用,不要用虛擬機測試!!因為 iOS 模擬器本身就是一台虛擬機,在虛擬機裡執行它會造成巢狀虛擬,導致網路功能完全失效而無法連線。

image image image

今日總結

今天,我們成功地與 AI 進行了第一次對話,我們建立了職責分離的 NetworkManagerGeminiAPIService,讓 ViewController 保持整潔。

明天,我們將基於這個堅實的基礎,打造第一個完整的 AI 專案:一個能將文字轉換成顏色的智慧配色 App


上一篇
Day 18 - 鑰匙與門戶:獲取你的第一個 AI API 金鑰並學會安全管理
下一篇
Day 20 - 【AI 專案 I】AI 設計師:打造智慧配色 App
系列文
iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言